Skip to content

fix(service-settings): a namespace reset clears the value stored beneath an env pin - #30793

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-30664-reset-clears-env-pinned
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-30664-reset-clears-env-pinned

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #30664

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

The rule: a namespace reset clears every stored value it can reach, the one beneath an env pin included, and says how many keys the environment still pins.

  • resetNamespace (@objectstack/service-settings). It built its patch from keys whose resolved source is a stored rung (global / tenant / user). A key an OS_* override in force pins resolves env, so a value stored beneath the pin was never cleared and took over again once the variable was removed, while the built-in reset said "environment/default configuration is back in effect". For a key that resolves env, the reset now reads the stored cascade beneath the pin (resolveStored, the half of get() below the env branch, extracted rather than copied, so get() is unchanged) and, when that names a stored rung, puts the key in the same all-null patch. The patch goes through setMany, whose env-lock branch already lets a clear through a pin since PR fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 (ADR-0007 D6 as amended), so there is no second bypass of the env lock: the clear meets the same row locks, platform-rung rule and write predicates a single clear meets. Nothing in force changes: the key still answers { source: 'env', locked: true }. A pinned key with nothing stored beneath it is not in the patch, so it writes no row and no audit entry. The count it returns includes the pinned keys it cleared; its signature is unchanged.
  • The reset action's answer (settings-action-copy.ts). runAction's built-in reset keeps resetCleared / resetNothing as they were (their wording is true now that nothing stored survives the reset) and, when some keys are still pinned, appends one new sentence, resetStillPinned(count), in all eleven languages: "2 setting(s) remain pinned by environment variables." Each sentence carries its own leading space where its language separates sentences with one (none in zh-CN, zh-Hant, ja-JP). A namespace no variable pins gets exactly the message it got before.
  • Audit / config-change rows. The reset of a pinned key goes through setMany's write loop like any clear, so it records exactly what a single clear of that key records: one reset row on each ledger (newHash: null) and one reset change event. Pinned in a test that compares the two writer rows field for field.

Tests

packages/services/service-settings/src/settings-service.test.ts, new top-level describe (5 cases), on the real SettingsService, with one key pinned over a stored value, one stored and not pinned, and one pinned with nothing stored:

  • the reset returns 2 (the pinned key counted), fires reset for those two keys only, both pinned keys still answer env and locked, and once the variables are removed both read their default;
  • the pinned key's audit row from the reset equals the one from a single set(…, null);
  • a pin with nothing stored beneath it is not cleared, and a second reset clears nothing and writes no audit row;
  • the reset action's message in English and zh-CN carries the pinned sentence (with the count) after the cleared or nothing-to-clear answer;
  • a namespace with no pins answers the old message byte for byte.

One existing case was renamed to what it pins now ("leaves an env-locked key with nothing stored beneath it untouched"); its assertions are unchanged. settings-copy-locale.test.ts gets a sample argument for resetStillPinned, so its existing every-language checks cover the new sentence.

Suites and typecheck (head 9b806cb)
  • pnpm --filter @objectstack/service-settings test: Test Files 70 passed (70), Tests 1206 passed (1206). typecheck (tsc --noEmit, includes src/**/*.test.ts): exit 0.
  • pnpm --filter @objectstack/plugin-auth exec vitest run src/allowed-ip-ranges.pin.test.ts src/auth-settings-clear.pin.test.ts (the two callers of resetNamespace outside the package, after building their closure): Test Files 2 passed, Tests 39 passed.
Ablations (each through `scripts/ablation-replace.mjs` on `settings-service.ts`: anchor 1 → 0, blob changed, restore blob == HEAD blob, `git diff HEAD` empty; the tests import the service from source, so no build sits between)
  • A. the reset reads the resolved source only, as before (const stored = v.source; /* ABLATION-30664-A */): 3 failed | 2 passed (the count and default-after-unpin case, the audit comparison, the action message); the two that pass pin what must not change (nothing stored beneath a pin; no pins at all).
  • B. the pinned sentence dropped (message: answer, /* ABLATION-30664-B */): 1 failed | 4 passed (the action message).

Acceptance notes

  • A reset now meets the row locks for a pinned key too. A pinned key whose stored value sits beneath a locked upper-rung row is now in the reset's patch, so setMany refuses the whole reset SETTINGS_LOCKED (locked-by-global / locked-by-tenant) for a caller below that rung, exactly as it already did for an unpinned key in that position. That is the clear's own rule from PR fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448, not a new one.
  • The reset clears at the key's declared rung. As before for unpinned keys, the clear writes null at the key's own rung for this caller; a value stored only at a higher rung the caller cannot write stays, and the key falls back to it once the pin goes. Unchanged behaviour, now applied to pinned keys as well.
  • Published surface. The action's message is the toast text the console prints and the HTTP action route returns; it gains one sentence only when keys are still pinned. No export, type, error code or route changes; resetNamespace keeps its signature.
  • Console. SettingsField still hides "Reset to default" on a pinned field; the namespace-level reset now reaches the value beneath it. Nothing in ui/ changes here.

Verification

All readings on head 9b806cbe (branched from origin/main 1371b9d8).

Derived gates: 60 derived, 59 exit 0, 1 NOT MEASURED
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 60 commands for the 5 changed paths; each ran with its exit code captured before any pipe. --ran reconciliation (recorded as command :: exit N): "60 derived famil(ies) accounted for — 59 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET (it needs a whole-workspace build; only the closures of service-settings and plugin-auth were built here). CI's Lint & Repo Gates carries it.
  • pnpm lint (eslint . --no-inline-config): exit 0.

Generated by Claude Code

…ath an env pin

resetNamespace skipped every key an OS_* override in force pins, so a
value stored beneath the pin survived the reset and came back when the
variable was removed. It now reads the stored cascade beneath the pin
and clears that value with the same null write setMany lets through a
pin, which changes nothing in force. The count includes those keys, and
the built-in reset action adds one sentence naming how many keys the
environment still pins, in all eleven languages.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cytLmJxfAmgPVjUbbW
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-settings, touching 8 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via SettingsService (symbol, a top-level class))
  • content/docs/protocol/kernel/config-resolution.mdx (via SettingsService (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via SettingsService (symbol, a top-level class))
  • content/docs/ui/actions.mdx (via runAction (symbol, a method of class SettingsService))
  • content/docs/ui/apps.mdx (via runAction (symbol, a method of class SettingsService))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via runAction (symbol, a method of class SettingsService))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 612468cc20e4d4adeba047d2416126ec9a8cc077 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3d45e193d45cbd1291c0a860d7b1ebf1f738b7f3 — the merge of head 9b806cbed6ea46f4746e580027316b3890becd4d into base 612468cc20e4d4adeba047d2416126ec9a8cc077, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3d45e193d45cbd1291c0a860d7b1ebf1f738b7f3 && git checkout 3d45e193d45cbd1291c0a860d7b1ebf1f738b7f3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 612468cc20e4d4adeba047d2416126ec9a8cc077 9b806cbed6ea46f4746e580027316b3890becd4d && git checkout -B drift-repro 612468cc20e4d4adeba047d2416126ec9a8cc077 && git merge --no-ff 9b806cbed6ea46f4746e580027316b3890becd4d

node scripts/docs-audit/affected-docs.mjs --json 612468cc20e4d4adeba047d2416126ec9a8cc077

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 612468cc20e4d4adeba047d2416126ec9a8cc077 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

席位核对 · ACCEPT · 2026-11-06T03:39Z · #30664

Head-sha: 9b806cb

  • Shape: 5 files, +182/−9: settings-service.ts, settings-action-copy.ts, two test files in packages/services/service-settings/, and one patch changeset. That is inside the card's scope. Not governed. One commit on origin/main 1371b9d87.
  • Premise, measured: on main, resetNamespace resets only keys whose source is global, tenant or user. A key pinned by an OS_* override answers env, so a value stored beneath the pin survived the reset and took over again once the variable was removed. That is the trap Allowed IP ranges: the documented env recovery cannot clear the stored list, so removing OS_AUTH_ALLOWED_IP_RANGES locks the admin out again at the next restart #30385 fixed for a single clear.
  • Spot check (read in the diff):
    • For a key answering env, the reset reads the stored cascade below the override through resolveStored. resolveStored is split out of get() with no behaviour change. If a value is stored there, the key joins the same all-null setMany patch, so the only door through the pin is the clear PR fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 already allows. No second write path exists.
    • The pins match the card's Expected:
      • A pinned key with a stored value is cleared, counted, and still answers env while the pin holds. Once the pin is lifted it reads its default, not the old value.
      • A pin with nothing stored beneath it is left alone, and a second reset clears nothing.
      • The audit rows from the reset path equal those from a single clear.
      • The answer adds the still-pinned sentence only when a key is pinned, and is byte-identical to main otherwise.
    • resetStillPinned is added in all eleven copy locales. The leading space follows each language's sentence join: none in zh-CN, zh-Hant or ja-JP, one in the others. es, fr, pt-BR and de inflect for 1 versus many.
    • SettingsActionCopy, SETTINGS_ACTION_COPY and resetNamespaceRows are not on the package barrel. resetNamespace keeps its signature.
  • Gates: every check-run on this head has concluded: 35 success, 13 skipped, 0 failure. Locally, per the dev report 6675962810:
    • service-settings: 70 files / 1206 tests; plugin-auth's two resetNamespace callers: 39 tests. Typecheck exit 0, pnpm lint exit 0.
    • Ablations: removing the pinned-key read fails 3 of the 5 new tests; dropping the sentence fails 1.
    • 60 derived gates: 59 exit 0. check:dual-build-cjs-loads needs a whole-workspace build, so it was not measured locally and was left to CI, where no check is red.
  • Accepted deviations: the new resetStillPinned sentence. The card's Expected asks the answer to say when keys are still pinned. The two existing sentences stay true, so they are left unchanged.
  • Note for i18n phase 3 (platform metadata and built-in emails): extraction and bundle checks read PLATFORM_LOCALES; backfill the seven missing locales #30512: settings A/B touch service-settings translations, not settings-action-copy.ts. If this lands first, settings A/B rebase over it with no overlap.
  • Contract: Clause-②: no, Metadata-protocol: none, confirmed from the diff. This is a behaviour fix on an existing method: no new export and no schema or key change.
  • Landing: ready, then auto-merge, through the relay. Settings reset: an env-pinned key keeps its stored value beneath the pin, while the reset says the namespace is back on environment/default configuration #30664 closes on merge (Fixes).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 03:40
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 6, 2026 03:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit a255a62 Nov 6, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-30664-reset-clears-env-pinned branch November 6, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant